import {
  CacheType,
  ChatInputCommandInteraction,
  SlashCommandBuilder,
  SlashCommandAttachmentOption,
  SlashCommandBooleanOption,
  SlashCommandChannelOption,
  SlashCommandIntegerOption,
  SlashCommandMentionableOption,
  SlashCommandNumberOption,
  SlashCommandRoleOption,
  SlashCommandStringOption,
  SlashCommandUserOption,
} from 'discord.js';
import EventEmitter from '@3xpo/events';
import { commandRegistry } from '../registerables/commandRegistry';
import type Bot from './bot';
import bot from '../bot';
import { logger } from '../util/logger';
import { Embed } from './embed';
import { UserPermissions as UserPermission } from '../util/db/models/user';

type CommandEvents = {
  run: (
    interaction: ChatInputCommandInteraction<CacheType>,
    bot: Bot,
    command: Command,
  ) => any | Promise<any>;
};
type Functionable<T> = ((t: T) => T) | T;
export default class Command extends EventEmitter<CommandEvents> {
  public constructor(protected builder: CommandBuilder) {
    super();
  }
  public get name() {
    return this.builder.name;
  }
  public get description() {
    return this.builder.description;
  }
  public get dmAvailability() {
    return this.builder.dmAvailability;
  }
  public get requiredPermissions() {
    return this.builder.requiredPermissions;
  }
  public get nsfw() {
    return this.builder.nsfw;
  }
  public get requiredBotPermissions() {
    return this.builder.requiredBotPermissions;
  }
  public get guildRestrictions() {
    return this.builder.guildRestrictions
      ? [...this.builder.guildRestrictions]
      : undefined;
  }
  // public get options() {
  //   return this.builder.options;
  // }
  public get aliases() {
    return [...this.builder.aliases];
  }
  public invoke(interaction: ChatInputCommandInteraction<CacheType>) {
    return this.emit('run', interaction, bot, this);
  }
  public on<K extends keyof CommandEvents>(
    event: K,
    listener: CommandEvents[K],
  ): this {
    return super.on(
      event,
      event === 'run'
        ? async (...a) => {
            try {
              return await listener(...a);
            } catch (error) {
              logger.error(
                `An error occurred while running command ${this.name}.`,
              );
              logger.error(error);
              const [interaction] = a;
              try {
                const embed = Embed.create().error(error);
                if (!interaction.replied)
                  interaction.reply({
                    embeds: [embed],
                  });
                else
                  interaction.followUp({
                    embeds: [embed],
                  });
              } catch (error) {
                logger.warn(
                  error,
                  'Failed to reply to user after encountering error',
                );
              }
            }
          }
        : listener,
    );
  }
  public get _builder() {
    return this.builder;
  }
}

/** Please collapse this generated class in your ide!!! for your own mental healths' sake! */
class _CommandOptionBuilder {
  protected builderFuncs = [] as ((builder: SlashCommandBuilder) => void)[];
  /** Function to internally manipulate the SlashCommandBuilder on build/export */
  public addBuilderHook(fn: (builder: SlashCommandBuilder) => void) {
    this.builderFuncs.push(fn);
    return this;
  }
  protected handleFunctionables<T>(
    getTToPass: () => T,
    ...fns: Functionable<T>[]
  ) {
    const arr: T[] = [];
    fns.forEach(opt => {
      if (typeof opt === 'function')
        // @ts-ignore
        arr.push(opt(getTToPass()));
      else arr.push(opt);
    });
    return arr;
  }
  // console.log(
  //   [
  //     'Attachment',
  //     'Boolean',
  //     'Channel',
  //     'Integer',
  //     'Mentionable',
  //     'Number',
  //     'Role',
  //     'String',
  //     'User',
  //   ]
  //     .map(
  //       v => `public add${v}Options(...options: Functionable<SlashCommand${v}Option>[]) {
  //   const values = this.handleFunctionables(
  //     ()=>new SlashCommand${v}Option(),
  //     ...options,
  //   );
  //   this.addBuilderHook(builder=>values.forEach(value=>builder.add${v}Option(value)));
  //   return this;
  // }
  // public add${v}Option(...option: Functionable<SlashCommand${v}Option>[]){return this.add${v}Options(...option)}
  // `,
  //     )
  //     .join('\n'),
  // );
  public addAttachmentOptions(
    ...options: Functionable<SlashCommandAttachmentOption>[]
  ) {
    const values = this.handleFunctionables(
      () => new SlashCommandAttachmentOption(),
      ...options,
    );
    this.addBuilderHook(builder =>
      values.forEach(value => builder.addAttachmentOption(value)),
    );
    return this;
  }
  public addAttachmentOption(
    ...option: Functionable<SlashCommandAttachmentOption>[]
  ) {
    return this.addAttachmentOptions(...option);
  }

  public addBooleanOptions(
    ...options: Functionable<SlashCommandBooleanOption>[]
  ) {
    const values = this.handleFunctionables(
      () => new SlashCommandBooleanOption(),
      ...options,
    );
    this.addBuilderHook(builder =>
      values.forEach(value => builder.addBooleanOption(value)),
    );
    return this;
  }
  public addBooleanOption(
    ...option: Functionable<SlashCommandBooleanOption>[]
  ) {
    return this.addBooleanOptions(...option);
  }

  public addChannelOptions(
    ...options: Functionable<SlashCommandChannelOption>[]
  ) {
    const values = this.handleFunctionables(
      () => new SlashCommandChannelOption(),
      ...options,
    );
    this.addBuilderHook(builder =>
      values.forEach(value => builder.addChannelOption(value)),
    );
    return this;
  }
  public addChannelOption(
    ...option: Functionable<SlashCommandChannelOption>[]
  ) {
    return this.addChannelOptions(...option);
  }

  public addIntegerOptions(
    ...options: Functionable<SlashCommandIntegerOption>[]
  ) {
    const values = this.handleFunctionables(
      () => new SlashCommandIntegerOption(),
      ...options,
    );
    this.addBuilderHook(builder =>
      values.forEach(value => builder.addIntegerOption(value)),
    );
    return this;
  }
  public addIntegerOption(
    ...option: Functionable<SlashCommandIntegerOption>[]
  ) {
    return this.addIntegerOptions(...option);
  }

  public addMentionableOptions(
    ...options: Functionable<SlashCommandMentionableOption>[]
  ) {
    const values = this.handleFunctionables(
      () => new SlashCommandMentionableOption(),
      ...options,
    );
    this.addBuilderHook(builder =>
      values.forEach(value => builder.addMentionableOption(value)),
    );
    return this;
  }
  public addMentionableOption(
    ...option: Functionable<SlashCommandMentionableOption>[]
  ) {
    return this.addMentionableOptions(...option);
  }

  public addNumberOptions(
    ...options: Functionable<SlashCommandNumberOption>[]
  ) {
    const values = this.handleFunctionables(
      () => new SlashCommandNumberOption(),
      ...options,
    );
    this.addBuilderHook(builder =>
      values.forEach(value => builder.addNumberOption(value)),
    );
    return this;
  }
  public addNumberOption(...option: Functionable<SlashCommandNumberOption>[]) {
    return this.addNumberOptions(...option);
  }

  public addRoleOptions(...options: Functionable<SlashCommandRoleOption>[]) {
    const values = this.handleFunctionables(
      () => new SlashCommandRoleOption(),
      ...options,
    );
    this.addBuilderHook(builder =>
      values.forEach(value => builder.addRoleOption(value)),
    );
    return this;
  }
  public addRoleOption(...option: Functionable<SlashCommandRoleOption>[]) {
    return this.addRoleOptions(...option);
  }

  public addStringOptions(
    ...options: Functionable<SlashCommandStringOption>[]
  ) {
    const values = this.handleFunctionables(
      () => new SlashCommandStringOption(),
      ...options,
    );
    this.addBuilderHook(builder =>
      values.forEach(value => builder.addStringOption(value)),
    );
    return this;
  }
  public addStringOption(...option: Functionable<SlashCommandStringOption>[]) {
    return this.addStringOptions(...option);
  }

  public addUserOptions(...options: Functionable<SlashCommandUserOption>[]) {
    const values = this.handleFunctionables(
      () => new SlashCommandUserOption(),
      ...options,
    );
    this.addBuilderHook(builder =>
      values.forEach(value => builder.addUserOption(value)),
    );
    return this;
  }
  public addUserOption(...option: Functionable<SlashCommandUserOption>[]) {
    return this.addUserOptions(...option);
  }
  ///////////////////////////////
}

export class CommandBuilder extends _CommandOptionBuilder {
  public constructor(public name: string, public description: string) {
    super();
  }
  public setName(name: string) {
    this.name = name;
    return this;
  }
  public setDescription(desc: string) {
    this.description = desc;
    return this;
  }
  public guildRestrictions: string[] | undefined;
  public setGuildRestrictions(guilds: string[]) {
    if (this.dmAvailability !== false)
      throw new Error(
        'Must call .setDmAvailability(false) before .setGuildRestrictions().',
      );
    this.guildRestrictions = guilds;
    return this;
  }
  public restrictToGuilds(...guilds: string[]) {
    return this.setGuildRestrictions(guilds);
  }
  public dmAvailability: boolean | null | undefined;
  public setDmAvailability(value: typeof this.dmAvailability) {
    this.dmAvailability = value;
    return this;
  }
  public requiredBotPermissions: UserPermission[] = [];
  public requireBotPermission(permission: UserPermission) {
    this.requiredBotPermissions.push(permission);
    return this;
  }
  public requiredPermissions: string | number | bigint | null | undefined;
  public setRequiredPermissions(value: typeof this.requiredPermissions) {
    this.requiredPermissions = value;
    return this;
  }
  public nsfw = false as boolean | undefined;
  public setNsfw(nsfw: boolean | undefined) {
    this.nsfw = nsfw;
    return this;
  }
  public aliases = [] as string[];
  public addAlias(...aliases: string[]) {
    this.aliases.push(...aliases);
  }
  public register() {
    const command = new Command(this);
    commandRegistry.set(this.name, command);
    this.aliases.forEach(n => commandRegistry.set(n, command));
    return command;
  }
  public export() {
    let builder = new SlashCommandBuilder()
      .setName(this.name)
      .setDescription(this.description);
    if (typeof this.dmAvailability !== 'undefined')
      builder.setDMPermission(this.dmAvailability);
    if (typeof this.requiredPermissions !== 'undefined')
      builder.setDefaultMemberPermissions(this.requiredPermissions);
    if (typeof this.nsfw !== 'undefined') builder.setNSFW(this.nsfw);
    //   [
    //     'Attachment',
    //     'Boolean',
    //     'Channel',
    //     'Integer',
    //     'Mentionable',
    //     'Number',
    //     'Role',
    //     'String',
    //     'User',
    //   ].forEach(v=>console.log(`this.${v.toLowerCase()}Options.forEach(v => builder.add${v}Option(v));`))
    this.builderFuncs.forEach(fn => fn(builder));
    return builder;
  }
}
